﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.ComponentModel;    // needed for Description Attributes/PropertyGrid
using System.Drawing.Design;    // UITypeEditor

/*
 * Actor : A force which affects fund(s) over time.
 * 
 * Actors play a primary role in Continuum.  They represent scheduled activity of fund transfers.
 * 
 * - Actors might represent income flowing in to a single fund at a predefined interval and amount.
 * - Actors might shift money amounts between two funds at a predefined interval and amount.
 * - Actors might represent fund disbursal whereby a money amount is removed from a single fund and lost (no longer in any tracked funds)
 * 
 * - Actors can utilize a Funds Historical Balances to fit/update their projections to fit actual balances (improve projection accuracy)
 * 
 * - Actors can optionally be designated as IsLoanPayment or IsCompoundInterestAccrual
 * - This designation will cause the primary fund to be reduced only by the calculated principle 
 * - while the feeder fund is deducted the full trigger amount (principle + embedded interest + overhead)
 * 
 * - Actors can optionally be designated as IsCompoundInterestAccrual
 * - This is pretty basic compound interest calculation based on APR and uses Periodicity for Compounding frequency
 */

namespace Continuum.Classes
{
    //public enum ActorTypeEnum { BillerDebt, BillerService, Depositor, Fund, Savings, Individual, Other };
    public enum ActorPeriodicityEnum { Daily, Weekly, Monthly, Yearly, OnceOnly };
 
    public class Actor
    {
        private string _ActorName;
        [Description("The name which the Actor is referred to")]
        public string ActorName { get { return _ActorName; } set { _ActorName = value; } }

        private string _ActorFundPrimary = "";
        [EditorAttribute(typeof(UITypeEditorClasses.FundsEditor), typeof(UITypeEditor))]
        [Description("The Primary Fund which the Actor acts upon")]
        public string ActorFundPrimary { get { return _ActorFundPrimary; } set { _ActorFundPrimary = value; _FundPrimaryReference = null; } }

        private string _ActorFundSecondary = "";
        [EditorAttribute(typeof(UITypeEditorClasses.FundsEditor), typeof(UITypeEditor))]
        [Description("The Secondary fund which the Actor acts upon")]
        public string ActorFundSecondary { get { return _ActorFundSecondary; } set { _ActorFundSecondary = value; _FundSecondaryReference = null; } }

        private ActorPeriodicityEnum _ActorPeriodicity;
        [Description("The Actor's Periodicity")]
        public ActorPeriodicityEnum ActorPeriodicity { get { return _ActorPeriodicity; } set { _ActorPeriodicity = value; } }

        private int _ActorPeriodicityUnits;
        [Description("The Actor's Periodicity Unit Multiplier (every n weeks)")]
        public int ActorPeriodicityUnits { get { return _ActorPeriodicityUnits; } set { _ActorPeriodicityUnits = value; } }

        private DateTime _TriggerDateInitial;
        [Description("The Initial Date which the Actor's trigger begins")]
        public DateTime TriggerDateInitial { get { return _TriggerDateInitial; } set { _TriggerDateInitial = value; } }

        private DateTime _TriggerDateEnd = DateTime.MaxValue;
        [Description("The Date which the Actor's trigger stops firing")]
        public DateTime TriggerDateEnd { get { return _TriggerDateEnd; } set { _TriggerDateEnd = value; } }

        private Decimal _TriggerAmount;
        [Description("The Amount/Amplitude by which the Actor's trigger affects cashflow")]
        public Decimal TriggerAmount { get { return _TriggerAmount; } set { _TriggerAmount = value; } }

        private Boolean _IsLoanPayment = false;
        [Description("Determines whether only principle should be posted.")]
        public Boolean IsLoanPayment { get { return _IsLoanPayment; } set { _IsLoanPayment = value; } }

        private Decimal _InterestRate = 4.00m;
        [Description("If IsLoan is true then this variable needs to be set to the APR interest rate of loan.")]
        public Decimal InterestRate { get { return _InterestRate; } set { _InterestRate = value; } }

        private Boolean _IsCompoundInterestAccrual = false;
        [Description("Determines if this actor represents compounded interest accrual")]
        public Boolean IsCompoundInterestAccrual { get { return _IsCompoundInterestAccrual; } set { _IsCompoundInterestAccrual = value; } }

        private Decimal _CompoundInterestRate = 2.00m;
        [Description("The APR interest rate for calculating accrual")]
        public Decimal CompoundInterestRate { get { return _CompoundInterestRate; } set { _CompoundInterestRate = value; } }

        private int _LoanDurationInYears = 0;
        [Description("If this actor is a loan payment this value helps determines interest")]
        public int LoanDurationInYears { get { return _LoanDurationInYears; } set { _LoanDurationInYears = value; } }

        private Boolean _IsDisabled = false;
        [Description("This property can be used to disable an actor from being used in projections.")]
        public Boolean IsDisabled { get { return _IsDisabled; } set { _IsDisabled = value; } }

        protected DateTime NextTriggerDate;

        // These references are for convenience and are not serialized (the fund names above, are instead)
        // These are optimized references, call Find() only once, unless the fund is changed
        private Fund _FundPrimaryReference = null;
        protected Fund FundPrimaryReference
        {
            get 
            {
                if (_FundPrimaryReference != null) return _FundPrimaryReference;
                if (_ActorFundPrimary == "") return null;

                _FundPrimaryReference = MainForm.projector.Funds.Find(delegate(Fund f) { return f.Name == ActorFundPrimary; });
                return _FundPrimaryReference;
            }

        }

        private Fund _FundSecondaryReference = null;
        protected Fund FundSecondaryReference
        {
            get 
            {
                if (_FundSecondaryReference != null) return _FundSecondaryReference;
                if (_ActorFundSecondary == "") return null;

                _FundSecondaryReference = MainForm.projector.Funds.Find(delegate(Fund f) { return f.Name == ActorFundSecondary; });
                return _FundSecondaryReference;
            }
        }

        public override string ToString()
        {
            return ActorName;
        }

        #region Constructors

        public Actor()
        {
        }

        // Single Fund
        public Actor(string ActorName, string ActorFundPrimary, ActorPeriodicityEnum ActorPeriodicity, int ActorPeriodicityUnits, DateTime TriggerDateInitial, Decimal TriggerAmount)
        {
            this.ActorName = ActorName;
            this.ActorFundPrimary = ActorFundPrimary;
            this.ActorPeriodicity = ActorPeriodicity;
            this.ActorPeriodicityUnits = ActorPeriodicityUnits;
            this.TriggerDateInitial = TriggerDateInitial;
            this.TriggerAmount = TriggerAmount;
        }

        // Multiple Fund
        public Actor(string ActorName, string ActorFundPrimary, string ActorFundSecondary, ActorPeriodicityEnum ActorPeriodicity, int ActorPeriodicityUnits, DateTime TriggerDateInitial, Decimal TriggerAmount)
        {
            this.ActorName = ActorName;
            this.ActorFundPrimary = ActorFundPrimary;
            this.ActorFundSecondary = ActorFundSecondary;
            this.ActorPeriodicity = ActorPeriodicity;
            this.ActorPeriodicityUnits = ActorPeriodicityUnits;
            this.TriggerDateInitial = TriggerDateInitial;
            this.TriggerAmount = TriggerAmount;
        }

        #endregion

        // Initial Periodicity is fixed to single date and needs to be cycled through time to any given date with affecting funds
        // So the product of this is really to set the NextTriggerDate
        // This has no effect on dependent funds, this is just to sync the trigger to a given date
        public void Synchronize(DateTime SyncDate) 
        {
            NextTriggerDate = TriggerDateInitial;

            if (TriggerDateInitial >= SyncDate) return;
            
            while (NextTriggerDate <= SyncDate) {
                switch (ActorPeriodicity) {
                    case ActorPeriodicityEnum.Daily : NextTriggerDate = NextTriggerDate.AddDays(ActorPeriodicityUnits); break;
                    case ActorPeriodicityEnum.Weekly: NextTriggerDate = NextTriggerDate.AddDays(ActorPeriodicityUnits * 7); break;
                    case ActorPeriodicityEnum.Monthly: NextTriggerDate = NextTriggerDate.AddMonths(ActorPeriodicityUnits); break;
                    case ActorPeriodicityEnum.Yearly: NextTriggerDate = NextTriggerDate.AddYears(ActorPeriodicityUnits); break;
                    case ActorPeriodicityEnum.OnceOnly: NextTriggerDate = DateTime.MaxValue; break;
                }

                // If next trigger occurs after the Trigger Ends, flag actor termination with NextTriggerDate as maxvalue
                if (TriggerDateEnd != DateTime.MaxValue && TriggerDateEnd < NextTriggerDate) NextTriggerDate = DateTime.MaxValue;
            }
        }

        // Projects all activities generated by this Actor for the given time frame (to be executed and analysed later)
        public List<Activity> ProjectActivities(DateTime StartDate, DateTime EndDate)
        {
            List<Activity> activities = new List<Activity>();

            Synchronize(StartDate);

            if (NextTriggerDate > EndDate) return activities;

            while (NextTriggerDate <= EndDate)
            {
                activities.Add(new Activity(NextTriggerDate, ActivityTypeEnum.Actor, this));

                switch (ActorPeriodicity)
                {
                    case ActorPeriodicityEnum.Daily: NextTriggerDate = NextTriggerDate.AddDays(ActorPeriodicityUnits); break;
                    case ActorPeriodicityEnum.Weekly: NextTriggerDate = NextTriggerDate.AddDays(ActorPeriodicityUnits * 7); break;
                    case ActorPeriodicityEnum.Monthly: NextTriggerDate = NextTriggerDate.AddMonths(ActorPeriodicityUnits); break;
                    case ActorPeriodicityEnum.Yearly: NextTriggerDate = NextTriggerDate.AddYears(ActorPeriodicityUnits); break;
                    case ActorPeriodicityEnum.OnceOnly: NextTriggerDate = DateTime.MaxValue; break;
                }

                // If next trigger occurs after the Trigger Ends, flag actor termination with NextTriggerDate as maxvalue
                if (TriggerDateEnd != DateTime.MaxValue && TriggerDateEnd < NextTriggerDate) NextTriggerDate = DateTime.MaxValue;
            }

            return activities;
        }

        // ExecuteActivity : replaces ProjectTime now that the projector's Activity list
        //      coordinates all activity chronologically.
        //
        // The projector Activity vector will use this method along with Actor.ProjectTriggers
        // This method will need to check to see if it IsLoan and amortize if needed.
        // Logic taken from : http://www.vbdotnetheaven.com/UploadFile/cruiz/VBDotNETBaseClasses04072005082328AM/VBDotNETBaseClasses.aspx
        public void ExecuteActivity(Activity activity)
        {
            if (FundPrimaryReference.FundClass == FundClassEnum.Debt || FundPrimaryReference.FundClass == FundClassEnum.DebtEquity)
            {
                if (FundPrimaryReference.InterimBalance.Amount <= 0.0m) return;
            }

            if (IsLoanPayment)
            {

                // Calculate Monthly Payment - using double type for Math.Pow
                double P = (double) FundPrimaryReference.InceptionBalance.Amount;
                double I = (double) InterestRate / 1200;
                double M = LoanDurationInYears * 12;
                double MonthlyPayment = P * (I / (1 - Math.Pow( (1 + I), -M) ));

                // If you know the Fund balance on the payment date you can calculate 
                // how much of that is interest... the rest goes to principle
               
                // Interest Portion = most recent Balance * Annual Interest rate / 12 (assumes monthly payments)
                // We are expecting InterimBalance to be kept up to date by our 
                //    chronological calls to PostVolatileTransaction
                decimal ActualInterestRate = InterestRate / 100;
                decimal InterestPaid = FundPrimaryReference.InterimBalance.Amount * ActualInterestRate / 12;
                decimal PrinciplePaid = (decimal) MonthlyPayment - InterestPaid;

                // Monthly Payment calculations dont include overhead of escrow, insurance, etc.
                // So the monthly payment calculation was only to calculate the principle
                // We will use Actor TriggerAmount as Monthly Payment

                // Only Posting Portion of Monthly payment added to Principle (Equity)
                activity.RenderedBalancePrimary = FundPrimaryReference.PostVolatile(PrinciplePaid, activity.ActivityDate, ActorName + " (Principle)", this);

                // Usually you will need feeder account but benchmark loans might not want to have one
                if (ActorFundSecondary != "")
                {
                    // split the pull from feeder fund into principle and interest for later analysis
                    FundSecondaryReference.PostVolatile(-(TriggerAmount - PrinciplePaid), activity.ActivityDate, ActorName + " (Overhead)", this);
                    activity.RenderedBalanceSecondary = FundSecondaryReference.PostVolatile(-PrinciplePaid, activity.ActivityDate, ActorName + " (Principle)", this);
                }

                return;
            }

            if (IsCompoundInterestAccrual)
            {
                if (ActorPeriodicity != ActorPeriodicityEnum.Monthly) {
                    throw new Exception("An interest accrual actor " + ActorName + " is not set to montly periodicity!");
                }

                Decimal CalculatedRate = ActorPeriodicityUnits / 12.0m * CompoundInterestRate / 100.0m;
                Decimal InterestAccrued = FundPrimaryReference.InterimBalance.Amount * CalculatedRate;

                activity.RenderedBalancePrimary = FundPrimaryReference.PostVolatile(InterestAccrued, activity.ActivityDate, ActorName, this);

                return;
            }

            // All other actors post only trigger amounts
            activity.RenderedBalancePrimary = FundPrimaryReference.PostVolatile(TriggerAmount, activity.ActivityDate, ActorName, this);
            if (FundSecondaryReference != null)
                activity.RenderedBalanceSecondary = FundSecondaryReference.PostVolatile(-TriggerAmount, activity.ActivityDate, ActorName, this);
        }
    }

}
